package com.hmdp.utils;


import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * 基于StringRedisTemplate封装一个缓存工具类，满足下列需求：
 *
 * * 方法1：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间
 * * 方法2：将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓
 *
 * 存击穿问题
 *
 * * 方法3：根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题
 * * 方法4：根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题
 */
@Component
public class RedisCacheUtils {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService es= Executors.newFixedThreadPool(10);

    private static final long SleepTime=50;

    /*
    将value对象序列化为json，缓存到redis中，并且设置过期时间
     */
    public void setWithExpire(String key, Object value, Long time, TimeUnit timeUnit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,timeUnit);
    }

    /*
    逻辑过期
     */
    public void setWithLogicExpire(String key, Object value, Long time, TimeUnit timeUnit){
        RedisData redisData = new RedisData();
        redisData.setData(value);
        //设置逻辑过期时间
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }

    /*
    解决缓存穿透
     */

    public <ID,R> R queryWithPassThrough(String prefix, Class<R> type,ID id, Long time, TimeUnit timeUnit, Function<ID,R> dbfallBack){
        String key=prefix+id;
        String value = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(value)){
            return JSONUtil.toBean(value,type);
        }
        if (value!=null){
            return null;
        }
        //缓存中不存在，从数据库中查找
        R res = dbfallBack.apply(id);
        if (res==null){
            //数据库中也不存在,证明该数据在数据库中也不存在，为了防止缓存穿透，在数据库中放一个"" 做为值
            stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        this.setWithExpire(key,res,time,timeUnit);
        return res;
    }

    /*
    通过锁+逻辑过期时间 解决 缓存击穿 问题
     */
    public <R,ID> R queryWithLogicExpire(String prefix,Class<R> type,ID id,Long time,TimeUnit timeUnit,Function<ID,R> dbFallBack){
        String key=prefix+id;
        String value = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(value)){
            //不存在，证明不是热点数据，直接返回
            return null;
        }
        RedisData redisData = JSONUtil.toBean(value, RedisData.class);
        R res = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        //在逻辑过期时间内，直接返回结果
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())){
            return res;
        }
        //已经逻辑过期
        //获取锁，成功的话，启动线程进行缓存重建，然后这个线程直接返回逻辑过期数据
        String lock_key=RedisConstants.LOCK_SHOP_KEY+id;
        if (tryLock(lock_key)){
            es.submit(()->{
                //双重检查

                //再次检查，redis中的逻辑过期时间是否被更新
                //如果已经被更新了，证明就不用更新了
                //否则，自己完成更新
                String value2=stringRedisTemplate.opsForValue().get(key);
                RedisData redisData1 = JSONUtil.toBean(value2, RedisData.class);
                if (redisData1.getExpireTime().isAfter(LocalDateTime.now())){
                    return;
                }
                try{
                    R r = dbFallBack.apply(id);
                    this.setWithLogicExpire(prefix+id,r,time,timeUnit);
                }catch (Exception E){
                    throw new RuntimeException(E);
                }finally {
                    //无论成功还是失败，都要释放锁
                    unlock(lock_key);
                }
            });
        }
        //没获取到锁，直接返回逻辑过期数据
        return res;
    }


    /*
    使用锁 解决 缓存击穿 问题
     */

    public <ID,R> R queryWithLock(String pre_fix,ID id,Class<R> type,Function<ID,R> dbfallBack,Long time,TimeUnit timeUnit){
        String key=pre_fix+id;
        String value= stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(value)){
            R res = JSONUtil.toBean(value, type);
            return res;
        }
        if (value!=null)
            return null;
        //缓存中不存在，重建
        String key_lock=RedisConstants.LOCK_SHOP_KEY+id;
        R r=null;
        try {
            boolean success = tryLock(key_lock);
            if (!success){
                //睡眠并重新尝试
                Thread.sleep(SleepTime);
                this.queryWithLock(pre_fix,id,type,dbfallBack,time,timeUnit);
            }
            //获取到锁，再次进行检查（双重检查）
            String value2= stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(value2))
                return JSONUtil.toBean(value2,type);
            //确实需要重建
            r = dbfallBack.apply(id);
            if (r==null){
                //数据库中也不存在，在redis中写入""避免缓存穿透，并且返回null
                stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //数据库中存在，重新写会redis，并且返回
            this.setWithExpire(key,r,time,timeUnit);
        }catch (Exception e){
            throw new RuntimeException(e);
        }finally {
            //无论如何 释放锁
            unlock(key_lock);
        }
        return r;
    }

    public boolean tryLock(String key){
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, "lock", 10, TimeUnit.SECONDS);
        return success;
    }

    public void unlock(String key){
        stringRedisTemplate.delete(key);
    }


}
